package fidl;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.franca.core.franca.FAnnotation;
import org.franca.core.franca.FAnnotationBlock;
import org.franca.core.franca.FArgument;
import org.franca.core.franca.FArrayType;
import org.franca.core.franca.FAttribute;
import org.franca.core.franca.FBasicTypeId;
import org.franca.core.franca.FBroadcast;
import org.franca.core.franca.FConstantDef;
import org.franca.core.franca.FEnumerationType;
import org.franca.core.franca.FEnumerator;
import org.franca.core.franca.FExpression;
import org.franca.core.franca.FField;
import org.franca.core.franca.FIntegerConstant;
import org.franca.core.franca.FInterface;
import org.franca.core.franca.FMapType;
import org.franca.core.franca.FMethod;
import org.franca.core.franca.FModel;
import org.franca.core.franca.FModelElement;
import org.franca.core.franca.FStructType;
import org.franca.core.franca.FType;
import org.franca.core.franca.FTypeCollection;
import org.franca.core.franca.FTypeDef;
import org.franca.core.franca.FTypeRef;
import org.franca.core.franca.FTypedElement;
import org.franca.core.franca.impl.FTypedElementImpl;

public class StoreGenerator {
	Configure configure = null;
	String filename = "";
	FModel model = null;
	FTypeCollection container = null;
	StringBuilder body = new StringBuilder();
	String indent = "";

	void increaseIndent() {
		indent += "\t";
	}

	void decreaseIndent() {
		indent = indent.substring(1);
	}
	
	boolean doGenerate(Resource resource, Configure configure) {
		this.configure = configure;
		this.filename = Utils.fetchFileName(resource);
		if (resource.getContents().isEmpty()) {
			System.out.println("An empty file: " + resource.getURI().path());
			return false;
		}
		EObject root = resource.getContents().get(0);
		if (root instanceof FModel) {
			model = (FModel) root;
		} else {
			System.out.println("Unsupport file: " + resource.getURI().path());
			return false;
		}
		// generate body first, because of we need know which types is import.
		String namespace = model.getName();
		if (!configure.PrefixNamespace.isEmpty()) {
			namespace = configure.PrefixNamespace + "." + namespace;
		}
		String[] namespaces = namespace.split("\\.");
		for (String item : namespaces) {
			body.append("namespace " + item + "\n{\n");
		}
		for (FTypeCollection typeCollection : model.getTypeCollections()) {
			generate(body, typeCollection);
		}
		for (FInterface interface_ : model.getInterfaces()) {
			generate(body, interface_);
		}
		for (int i = namespaces.length; i != 0; --i) {
			body.append("} // namespace " + namespaces[i - 1] + "\n");
		}
		// generate head.
		StringBuilder head = new StringBuilder();
		head.append("// Generated by FrancaIDL tool, Do **NOT** Modify!\n\n" +
			"#pragma once\n\n" +
			"#include <cstdint>\n" +
			"#include <string>\n" +
			"#include <vector>\n" +
			"#include <map>\n\n" +
			"#include \"Interface.hpp\"\n");
		EList<Resource> resources = resource.getResourceSet().getResources();
		for (Resource item : resources) {
			if (item == resource) continue;
			if (item.getContents().isEmpty()) continue;
			EObject object = item.getContents().get(0);
			if (object instanceof FModel) {
				generateImport(head, (FModel) object);
			}
		}
		head.append("\n");
		body.insert(0, head);
		return true;
	}

	boolean writeToFile(String directory) {
		String namespace = model.getName();
		if (!configure.PrefixNamespace.isEmpty()) {
			namespace = configure.PrefixNamespace + "." + namespace;
		}
		BufferedWriter bufferedWriter = null;
		try {
			String filename = directory + "/" + namespace.replace(".", "/") +
				"/" + this.filename + ".hpp";
			File file = Utils.createFile(filename);
			bufferedWriter = new BufferedWriter(new FileWriter(file));
			bufferedWriter.write(body.toString());
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		} finally {
			try {
				if (bufferedWriter != null) bufferedWriter.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return true;
	}

	boolean generate(StringBuilder builder, FTypeCollection typeCollection) {
		container = typeCollection;
		builder.append("\n");
		generateCommet(builder, typeCollection.getComment());
		String name = typeCollection.getName();
		if (name == null) {
			System.out.println("warning: typeCollection name is null.");
		} else {
			builder.append("namespace " + name + "\n{\n\n");
		}
		for (FType type : typeCollection.getTypes()) {
			generateType(builder, type);
		}
		for (FConstantDef constant : typeCollection.getConstants()) {
			generateConstant(builder, constant);
		}
		if (name != null) {
			builder.append("} // namespace " + name + "\n");
		}
		return true;
	}

	boolean generate(StringBuilder builder, FInterface interface_) {
		container = interface_;
		builder.append("\n");
		generateCommet(builder, interface_.getComment());
		String name = interface_.getName();
		if (name == null) {
			System.out.println("Interface name can not be null.");
			return false;
		} else {
			builder.append(indent + "class " + name + "\n");
			builder.append(indent + "{\n" + indent + "public:\n");
		}
		increaseIndent();
		for (FType type : interface_.getTypes()) {
			generateType(builder, type);
		}
		for (FConstantDef constant : interface_.getConstants()) {
			generateConstant(builder, constant);
		}
		for (FAttribute attribute : interface_.getAttributes()) {
			generateAttribute(builder, attribute);
		}
		for (FBroadcast broadcast : interface_.getBroadcasts()) {
			generateBroadcast(builder, broadcast);
		}
		for (FMethod method : interface_.getMethods()) {
			generateMethod(builder, method);
		}
		decreaseIndent();
		builder.append(indent + "}; // class " + name + "\n\n");
		// generate store
		builder.append(indent + "class " + name + "Store final " +
			": public IStore, public " + name + "\n" +
			indent + "{\n" + indent + "public:\n");
		increaseIndent();
		builder.append(indent + "explicit " + name +
			"Store(IContext *ctx) : IStore(ctx) {}\n\n");
		for (FAttribute attribute : interface_.getAttributes()) {
			generateCommet(builder, attribute.getComment());
			builder.append(indent + "Attribute<" + attribute.getName() + "> " +
				"m_" + attribute.getName() + "{this};\n");
		}
		for (FBroadcast broadcast : interface_.getBroadcasts()) {
			generateCommet(builder, broadcast.getComment());
			builder.append(indent + "Broadcast<" + broadcast.getName() + "> " +
				"m_" + broadcast.getName() + "{this};\n");
		}
		for (FMethod method : interface_.getMethods()) {
			generateCommet(builder, method.getComment());
			builder.append(indent + "Method<" + method.getName() + "> " +
				"m_" + method.getName() + "{this};\n");
		}
		decreaseIndent();
		builder.append(indent + "};\n\n");
		return true;
	}

	void generateBroadcast(StringBuilder builder, FBroadcast broadcast) {
		generateCommet(builder, broadcast.getComment());
		builder.append(indent + "struct " + broadcast.getName() + "\n");
		builder.append(indent + "{\n");
		increaseIndent();
		for (FArgument argument : broadcast.getOutArgs()) {
			generateCommet(builder, argument.getComment());
			FTypeRef typeRef = argument.getType();
			builder.append(indent + resovleTypeName(typeRef) +
				" m_" + argument.getName() + "{};\n");
		}
		generateEqual(builder, broadcast.getName(), broadcast.getOutArgs());
		decreaseIndent();
		builder.append(indent + "};\n\n");
	}

	void generateAttribute(StringBuilder builder, FAttribute attribute) {
		FTypeRef typeRef = attribute.getType();
		FType baseType = typeRef.getDerived();
		String name = attribute.getName();
		if (baseType != null &&
			baseType.eContainer() == attribute.eContainer() &&
			baseType.getName().equals(name)) {
			// attribute is same of attribute type
			return;
		}
		String typeName = resovleTypeName(typeRef);
		generateCommet(builder, attribute.getComment());
		builder.append(indent + "struct " + name + "\n");
		builder.append(indent + "{\n");
		increaseIndent();
		builder.append(indent + typeName + " m_value{};\n");
		// constructor
		builder.append(indent + name + "() = default;\n");
		builder.append(indent + name + "(const " + typeName +
			" &value) : m_value(value) {}\n");
		List<FTypedElement> members = new ArrayList<FTypedElement>();
		members.add(new FTypedElementImpl() {
			@Override
			public String getName() {
				return "value";
			}
		});
		generateEqual(builder, name, members);
		decreaseIndent();
		builder.append(indent + "};\n\n");
	}

	void generateMethod(StringBuilder builder, FMethod method) {
		generateCommet(builder, method.getComment());
		builder.append(indent + "struct " + method.getName() + "\n");
		builder.append(indent + "{\n");
		increaseIndent();
		builder.append(indent + "struct Argument\n");
		builder.append(indent + "{\n");
		increaseIndent();
		for (FArgument argument : method.getInArgs()) {
			generateCommet(builder, argument.getComment());
			FTypeRef typeRef = argument.getType();
			builder.append(indent + resovleTypeName(typeRef) +
				" m_" + argument.getName() + "{};\n");
		}
		decreaseIndent();
		builder.append(indent + "};\n");
		builder.append(indent + "struct Result\n");
		builder.append(indent + "{\n");
		increaseIndent();
		for (FArgument argument : method.getOutArgs()) {
			generateCommet(builder, argument.getComment());
			FTypeRef typeRef = argument.getType();
			builder.append(indent + resovleTypeName(typeRef) +
				" m_" + argument.getName() + "{};\n");
		}
		decreaseIndent();
		builder.append(indent + "};\n");
		builder.append(indent + "enum class Error: int\n");
		builder.append(indent + "{\n");
		increaseIndent();
		generateCommet(builder, method.getErrorComment());
		FEnumerationType enumeration = method.getErrors();
		for (FEnumerator enumerator : enumeration.getEnumerators()) {
			generateCommet(builder, enumerator.getComment());
			builder.append(indent + enumerator.getName());
			FExpression expression = enumerator.getValue();
			if (expression instanceof FIntegerConstant) {
				FIntegerConstant value = (FIntegerConstant) expression;
				builder.append(" = " + value.getVal());
			}
			builder.append(",\n");
		}
		decreaseIndent();
		builder.append(indent + "};\n");
		decreaseIndent();
		builder.append(indent + "};\n\n");
	}

	void generateConstant(StringBuilder builder, FConstantDef constant) {
		generateCommet(builder, constant.getComment());
		FTypeRef typeRef = constant.getType();
		builder.append("#error I dont know what it is, so I do not deal it.");
		builder.append(indent + resovleTypeName(typeRef) + " " +
			constant.getName() + " = " + constant.getRhs().toString() + "\n\n");
	}

	void generateType(StringBuilder builder, FType type) {
		generateCommet(builder, type.getComment());
		if (type instanceof FStructType) {
			FStructType struct = (FStructType) type;
			builder.append(indent + "struct " + struct.getName() + "\n");
			builder.append(indent + "{\n");
			increaseIndent();
			for (FField member : struct.getElements()) {
				generateCommet(builder, member.getComment());
				String typeName = resovleTypeName(member.getType());
				builder.append(indent + typeName + " m_" +
					member.getName() + "{};\n");
			}
			generateEqual(builder, struct.getName(), struct.getElements());
			decreaseIndent();
			builder.append(indent + "};\n\n");
		} else if (type instanceof FTypeDef) {
			FTypeDef typeDef = (FTypeDef) type;
			String typeName = resovleTypeName(typeDef.getActualType());
			builder.append(indent + "typedef " + typeName + " " +
				typeDef.getName() + ";\n\n");
		} else if (type instanceof FMapType) {
			FMapType mapType = (FMapType) type;
			String keyName = resovleTypeName(mapType.getKeyType());
			String valueName = resovleTypeName(mapType.getValueType());
			builder.append(indent + "typedef std::map<" + keyName + ", " +
				valueName + ">" + " " + mapType.getName() + ";\n\n");
		} else if (type instanceof FArrayType) {
			FArrayType arrayType = (FArrayType) type;
			String eleName = resovleTypeName(arrayType.getElementType());
			builder.append(indent + "typedef std::vector<" + eleName + "> " +
				arrayType.getName() + ";\n\n");
		} else if (type instanceof FEnumerationType) {
			FEnumerationType enumeration = (FEnumerationType) type;
			builder.append(indent + "enum class " + type.getName() + ": int\n");
			builder.append(indent + "{\n");
			increaseIndent();
			for (FEnumerator enumerator : enumeration.getEnumerators()) {
				generateCommet(builder, enumerator.getComment());
				builder.append(indent + enumerator.getName());
				FExpression expression = enumerator.getValue();
				if (expression instanceof FIntegerConstant) {
					FIntegerConstant value = (FIntegerConstant) expression;
					builder.append(" = " + value.getVal());
				}
				builder.append(",\n");
			}
			decreaseIndent();
			builder.append(indent + "};\n\n");
		}
	}

	void generateCommet(StringBuilder builder, FAnnotationBlock annotation) {
		if (annotation == null) {
			return;
		}
		EList<FAnnotation> commets = annotation.getElements();
		for (FAnnotation commet : commets) {
			String[] lines = commet.getComment().split("\n");
			for (String line : lines) {
				if (line.isBlank()) {
					builder.append(indent + "//\n");
				} else {
					builder.append(indent + "// " + line.trim() + "\n");
				}
			}
		}
	}

	void generateImport(StringBuilder builder, FModel importModel) {
		String[] modelNames = model.getName().split("\\.");
		String[] importModelNames = importModel.getName().split("\\.");
		builder.append("#include \"");
		for (int i = 0; i < importModelNames.length; ++i) {
			if (i < modelNames.length) {
				// a.b.c, a.c.d.e
				if (importModelNames[i].equals(modelNames[i]))
					continue;
				for (int j = 0; j < modelNames.length - i; ++j) {
					builder.append("../");
				}
			}
			for (int j = i; j < importModelNames.length; ++j) {
				builder.append(importModelNames[j]);
				builder.append("/");
			}
			break;
		}
		builder.append(Utils.fetchFileName(importModel.eResource()));
		builder.append(".hpp\"\n");
	}

	<T extends FTypedElement>
	void generateEqual(StringBuilder builder, String typeName, List<T> args) {
		builder.append("\n");
		builder.append(indent + "inline bool operator==(const " +
			typeName + " &rh)\n" + indent + "{\n");
		increaseIndent();
		if (args.isEmpty()) { // maybe empty? I don't know, return true anyway.
			builder.append(indent + "return true;\n");
		} else {
			builder.append(indent + "return (");
			increaseIndent();
			for (Iterator<T> itr = args.iterator(); itr.hasNext();) {
				String name = itr.next().getName();
				builder.append("m_" + name + " == rh.m_" + name);
				if (itr.hasNext()) {
					builder.append(" &&\n" + indent);
				}
			}
			builder.append(");\n");
			decreaseIndent();
		}
		decreaseIndent();
		builder.append(indent + "}\n");
		builder.append(indent + "inline bool operator!=(const " +
			typeName + " &rh)\n" + indent + "{\n");
		increaseIndent();
		builder.append(indent + "return !(*this == rh);\n");
		decreaseIndent();
		builder.append(indent + "}\n");
		String fullTypeName = model.getName();
		if (!container.getName().isEmpty()) {
			fullTypeName += "." + container.getName();
		}
		fullTypeName += "." + typeName;
		builder.append(indent + "inline const char *typeName() const\n" +
					   indent + "{\n");
		increaseIndent();
		builder.append(indent + "return \"" + fullTypeName + "\";\n");
		decreaseIndent();
		builder.append(indent + "}\n");
	}

	String resovleTypeName(FTypeRef type) {
		FBasicTypeId id = type.getPredefined();
		switch (id.getValue()) {
		case FBasicTypeId.BOOLEAN_VALUE:
			return "bool";
		case FBasicTypeId.INT8_VALUE:
		case FBasicTypeId.INT16_VALUE:
		case FBasicTypeId.INT32_VALUE:
			return "int32_t";
		case FBasicTypeId.INT64_VALUE:
			return "int64_t";
		case FBasicTypeId.UINT8_VALUE:
		case FBasicTypeId.UINT16_VALUE:
		case FBasicTypeId.UINT32_VALUE:
			return "uint32_t";
		case FBasicTypeId.UINT64_VALUE:
			return "uint64_t";
		case FBasicTypeId.FLOAT_VALUE:
			return "float";
		case FBasicTypeId.DOUBLE_VALUE:
			return "double";
		case FBasicTypeId.STRING_VALUE:
		case FBasicTypeId.BYTE_BUFFER_VALUE:
			return "std::string";
		default:
			return resovleTypeName(type.getDerived());
		}
	}

	String resovleTypeName(FModelElement baseType) {
		if (baseType == null) {
			return "undefined";
		}
		String fullName = baseType.getName();
		while (true) {
			EObject object = baseType.eContainer();
			if (container == object) {
				break;
			}
			if (object instanceof FModelElement) {
				baseType = (FModelElement) object;
				String typeName = baseType.getName();
				if (typeName != null) {
					fullName = typeName + "." + fullName;
				}
			} else if (object instanceof FModel) {
				FModel containerModel = (FModel) object;
				if (containerModel != model) {
					fullName = containerModel.getName() + "." + fullName;
				}
				break;
			} else {
				return "undefined";
			}
		}
		fullName = fullName.replaceAll("\\.", "::");
		return fullName;
	}
}
